/**
 *
 * The MIT License
 *
 * Copyright (c) 2008 Board of Trustees, Leland Stanford Jr. University. All rights reserved.
 * Copyright (c) 2009 Ben Suter. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.macdadi.ks.client.views;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import org.macdadi.core.client.auth.AuthService;
import org.macdadi.core.client.auth.AuthenticatedUser;
import org.macdadi.core.client.auth.AuthorizationException;
import org.macdadi.core.client.domain.*;
import org.macdadi.core.client.ui.DefaultItemEditor;
import org.macdadi.core.client.ui.EditableTree;
import org.macdadi.ks.client.View;
import org.macdadi.ks.client.ViewContainer;
import org.macdadi.ks.client.components.StakeholderList;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * @author Ben Suter <ben.suter@gmail.com>
 */
public class Preferences extends View implements TreeListener, StakeholderList.StakeholderListListener
{
    protected final String CHART_URL_BASE = "http://chart.apis.google.com/chart?";
    protected VerticalPanel chartPanel;

    protected VerticalPanel leftPanel;

    protected int selectedStakeholderId;
    protected boolean isGroupSelected;

    protected StakeholderList stakeholderSelect;
    protected EditableTree tree;
    protected ArrayList expandedItems = new ArrayList();
    protected ProjectInfo project;
    protected AuthenticatedUser currentUser;

    protected EditableTree.ItemEditor preferenceEdit;

    protected HashMap prefsByGoal = new HashMap();
    protected final int maxBudget = 100;
    protected int remainingBudget;

    public static ViewInfo init()
    {
        String info = "<p>Define your preferences for each goal</p>";

        return new ViewInfo("Preferences", info)
        {
            public View createInstance(ViewContainer container)
            {
                return new Preferences(container);
            }

            public String getColor()
            {
                return "#2fba10";
            }
        };
    }

    public Preferences(ViewContainer container)
    {
        this.container = container;
        project = container.getCurrentProject();

        /* Initialize to all users */
        selectedStakeholderId = -1;
        isGroupSelected = false;

        HorizontalPanel mainPanel = new HorizontalPanel();
        mainPanel.setStyleName("macdadi-Preferences");
        mainPanel.setWidth("100%");

        leftPanel = new VerticalPanel();
        mainPanel.add(leftPanel);

        stakeholderSelect = new StakeholderList(container);
        stakeholderSelect.setListener(this);
        leftPanel.add(stakeholderSelect);

        tree = new EditableTree();
        leftPanel.add(tree);
        tree.addTreeListener(this);

        preferenceEdit = new PreferenceEdit(updateView, project);

        chartPanel = new VerticalPanel();
        mainPanel.add(chartPanel);

        initWidget(mainPanel);
    }

    public void onShow() {
        currentUser = container.getCurrentUser();

        /* Whichever entry is selected in this dropdown will trigger the tree and chart to be populated. */
        stakeholderSelect.populate(project.getId(), true);
    }

    public void onHide() {
        tree.stopEditing();
        tree.removeItemEditor(preferenceEdit);
        tree.clear();
        chartPanel.clear();
    }

    /**
     * A command that refreshes the view (tree and chart).
     */
    protected Command updateView = new Command() {
        public void execute() {
            tree.stopEditing();
            tree.clear();
            chartPanel.clear();
            populateTree(project);
            populateChart();
        }
    };


    /* Configure the editor for the preference tree: make the tree editable if the
     * current user's data is being displayed and they are authorized to edit.
     */
    protected void configurePreferenceEditor() {
        if ( ! isGroupSelected && currentUser != null && currentUser.getId() == selectedStakeholderId && currentUser.isAuthorizedOperation(project.getId(), AuthService.TARGET_PREFERENCES, AuthService.OPERATION_UPDATE)) {
            tree.addItemEditor(preferenceEdit);
        } else {
            tree.removeItemEditor(preferenceEdit);
        }
    }

    /**
     * Add a new root item to the tree, whose label initially displays a "loading ..." message.
     * Then it retrieves all the root-level goals for this project and adds each one to the
     * tree. As it does so, the children of these goals are added recursively. See addChild().
     *
     * Finally the rootItem is expanded, so that the first level of goals
     * is always visible.
     *
     * @param project the project from which the tree domain should be pulled
     */
    protected void populateTree(final ProjectInfo project) {

        tree.clear();

        configurePreferenceEditor();
        
        final TreeItem rootItem = new TreeItem();
        rootItem.setText("Loading goals with preferences");
        tree.addItem(rootItem);

        if ( isGroupSelected && selectedStakeholderId >= 0 ) {
            DataService.App.getInstance().getPreferencesForStakeholderGroup(project.getId(), selectedStakeholderId, container.getCurrentAuthToken(), new PreferencesTreeCallback(rootItem));
        } else if ( selectedStakeholderId >= 0 ) {
            DataService.App.getInstance().getPreferencesForUser(project.getId(), selectedStakeholderId, container.getCurrentAuthToken(), new PreferencesTreeCallback(rootItem));
        } else if ( selectedStakeholderId == -1 ) {
            DataService.App.getInstance().getPreferencesForProject(project.getId(), container.getCurrentAuthToken(), new PreferencesTreeCallback(rootItem));
        }
    }

    protected class PreferencesTreeCallback implements AsyncCallback
    {
        TreeItem rootItem;

        public PreferencesTreeCallback(TreeItem rootItem)
        {
            this.rootItem = rootItem;
        }

        public void onFailure(Throwable caught)
        {
            GWT.log("Failed to populate the preferences tree", caught);
            if (caught instanceof AuthorizationException) {
                rootItem.setText("Please login to view preferences for " + project.getName());
            }
        }

        public void onSuccess(Object result)
        {
            Preference[] prefs = (Preference[]) result;

            remainingBudget = maxBudget;
            prefsByGoal.clear();

            Preference pref;
            for (int i = 0; i < prefs.length; i++) {
                pref = prefs[i];
                prefsByGoal.put(Integer.toString(pref.getGoal().getId()), pref);
                remainingBudget -= pref.getValue();
            }
            loadGoalsWithPreferences(rootItem);
        }
    }

    protected void loadGoalsWithPreferences(final TreeItem rootItem) {
        DataService.App.getInstance().getGoals(project.getId(), container.getCurrentAuthToken(), new AsyncCallback() {
            public void onFailure(Throwable caught) {
                GWT.log("Failed to populate the goals/preferences tree", caught);
                if (caught instanceof AuthorizationException) {
                    rootItem.setText("Please login to view preferences for " + project.getName());
                }
            }

            public void onSuccess(Object result) {
                if ( ! isGroupSelected && selectedStakeholderId >= 0 ) {
                    rootItem.setText("Preferences - Remaining Budget: " + remainingBudget);
                } else {
                    rootItem.setText("Aggregate Preferences");
                }

                Goal[] roots = (Goal[]) result;
                for (int i = 0; i < roots.length; i++) {
                    addChild(rootItem, roots[i], true);
                }

                /* Expand the first and second levels */
                rootItem.setState(true, true);
                for (int i = 0; i < rootItem.getChildCount(); i++ ) {
                    rootItem.getChild(i).setState(true, true);
                }
            }
        });
    }

    /**
     * Adds the given Goal as a child TreeItem to the parent TreeItem.
     * Optionally steps into the given Goal's children and does the same there,
     * etc. recursively.
     *
     * @param parent  the tree node to act as parent for the newly added goal
     * @param child   the goal to be added as a child node to the tree
     * @param recurse if true, will call this method for the child and each of its children, etc.
     *
     * @return the aggregated preference value of this node
     */
    protected int addChild(TreeItem parent, Goal child, boolean recurse) {
        TreeItem node = new TreeItem();
        node.setUserObject(child);
        parent.addItem(node);

        int aggregateValue = 0;
        if (recurse) {
            for (int i = 0; i < child.getNumChildren(); i++)
                aggregateValue += addChild(node, child.getChild(i), true);
        }


        Preference pref;
        pref = (Preference) prefsByGoal.get(Integer.toString(child.getId()));
        if ( pref != null) {
            /* If this goal has a specific preference assigned, its value is taken
             * as the associated preference. Otherwise its value is taken as the
             * aggregated values of its sub-goals.
             */
            aggregateValue = pref.getValue();
        }

        String label;
        label = child.getName();
        label += ": " + aggregateValue;
        node.setText(label);

        /* If this node, as identified by its goal's id, was "expanded" in a
         * previous incarnation of this tree, then expand it here again.
         */
        if (expandedItems.contains(Integer.toString(child.getId())))
            node.setState(true, false);

        return aggregateValue;
    }


    public void onTreeItemSelected(TreeItem item) {
        // do nothing
    }

    /**
     * Remember the expansion state of this item, so that we can later
     * return the tree to the same state when it is refreshed.
     *
     * @param item the item whose expanded/folded state changed
     */
    public void onTreeItemStateChanged(TreeItem item) {
        /* Remember the expansion state for later tree refresh, based on goal ID. */
        Goal g;
        if (item.getUserObject() instanceof Goal)
            g = (Goal) item.getUserObject();
        else
            return;

        if (item.getState())
            expandedItems.add(Integer.toString(g.getId()));
        else
            expandedItems.remove(Integer.toString(g.getId()));
    }



    /**
     * Display aggregate preference values, as a pie chart. All leaf
     * nodes are aggregated and each of their parents is displayed as
     * a piece of the pie.
     */
    protected void populateChart()
    {
        chartPanel.clear();

        if ( isGroupSelected && selectedStakeholderId >= 0 ) {
            DataService.App.getInstance().getAggregatePreferencesForStakeholderGroup(project.getId(), selectedStakeholderId, container.getCurrentAuthToken(), new PreferencesChartCallback());
        } else if ( selectedStakeholderId >= 0 ) {
            DataService.App.getInstance().getAggregatePreferencesForUser(project.getId(), selectedStakeholderId, container.getCurrentAuthToken(), new PreferencesChartCallback());
        } else if ( selectedStakeholderId == -1 ) {
            DataService.App.getInstance().getAggregatePreferencesForProject(project.getId(), container.getCurrentAuthToken(), new PreferencesChartCallback());
        }
    }

    protected class PreferencesChartCallback implements AsyncCallback
    {
         public void onFailure(Throwable caught)
            {
                GWT.log("Failed to populate the preferences chart", caught);
                if (caught instanceof AuthorizationException) {
                    // TODO: do not use Window.alert for error feedback
                    Window.alert("Please login to view preferences for " + project.getName());
                }
            }

            public void onSuccess(Object result)
            {
                Preference[] prefs = (Preference[]) result;
                drawChart(prefs);
            }
    }

    protected void drawChart(Preference[] prefs)
    {
        int numPrefs = prefs.length;
        String[] goalNames = new String[numPrefs];
        double[] goalPrefs = new double[numPrefs];

        Preference p;
        for (int i = 0; i < numPrefs; i++ ) {
            p = prefs[i];
            goalNames[i] = p.getGoal().getName();
            goalPrefs[i] = p.getValue();
        }

        // Example URL for pie chart:
        // http://chart.apis.google.com/chart?chs=600x230&cht=p&chtt=Preferences%20(Martha)&chco=00cc00&chd=t:22,18,14,14,14,10,8&chl=
        // =Promote%20Interaction|Maximize%20Human%20Comfort|Reduce%20Environmental%20Impact|Be%20Economically%20Responsible|Inspire|Accommodate%20Flexible%20Programming|Participate%20in%20Sustainability%20Movement
        // And for aggregate across all stakeholders
        // http://chart.apis.google.com/chart?cht=p&chs=600x230&chco=2fba10&chtt=Preferences
        // &chd=t:318.0,316.0,281.0,213.0,202.0,163.0,85.0&chds=0,318.0
        // &chl=Maximize Human Comfort|Reduce Environmental Impact|Promote Interaction|Inspire|Accommodate Flexible Programming|Be Economically Responsible|Participate in Sustainability Movement

        StringBuffer url = new StringBuffer();
        url.append(CHART_URL_BASE);
        url.append("cht=p"); // pie chart
        url.append("&chs=600x230"); // size in pixels, note this may need to be larger for longer goal names, etc.
        url.append("&chco=2fba10"); // color is same as view, green
        url.append("&chtt=Preferences"); // title

        /* Set the data, e.g. &chd=t:-28,-27,11,2,-1&chds=-80,140
         * We use "Text encoding with data scaling", see
         * http://code.google.com/apis/chart/formats.html#data_scaling
         */
        double maxPref = 0;
        double thisPref;
        url.append("&chd=t:");
        for (int i = 0; i < goalPrefs.length; i++) {
            if ( i > 0 )
                url.append(",");

            thisPref = goalPrefs[i];
            url.append(thisPref);

            if ( thisPref > maxPref) {
                maxPref = thisPref; /* Keep track of the max value */
            }

        }
        /* Data scaling:
         *  our prefs are always >= zero, so the minimum scale value is 0
         *  we set the maximum scale value equal to the maximum pref value in our data
         */
        url.append("&chds=0,").append(maxPref);

        /* Set the labels, e.g. &chl=Something|Another thing|Last item */
        url.append("&chl=");
        for (int i = 0; i < goalNames.length; i++) {
            if ( i > 0 )
                url.append("|");

            url.append(goalNames[i]);
        }

        HTML chart = new HTML("<img src=\"" + url.toString() + "\" title=\"Preferences chart\" />");
        chartPanel.add(chart);
    }

    /**
     * Respond to changes in the StakeholderList selection, filling the tree and chart as appropriate.
     * @param selectedID
     * @param isGroup
     */
    public void selectionChanged(int selectedID, boolean isGroup) {
        selectedStakeholderId = selectedID;
        isGroupSelected = isGroup;

        if (isGroupSelected && selectedStakeholderId < 0 ) {
            GWT.log("The combination of group without valid id is an error", null);
            tree.clear();
            chartPanel.clear();
            return;           
        }

        populateTree(project);
        populateChart();
    }


    /**
     * Lets you edit a user preference value for a goal.
     * Only matches to leaf goals.
     */
    public class PreferenceEdit extends DefaultItemEditor {
        protected ProjectInfo project;

        public PreferenceEdit(Command onSuccess, ProjectInfo p) {
            super(DefaultItemEditor.iconPathEdit, "Set preference", onSuccess);
            this.project = p;
        }

        protected Widget getInputWidget(Object userObject) {
            if (userObject == null || !(userObject instanceof Goal))
                throw new IllegalArgumentException("userObject must be a valid Goal");

            return configureWidget((Goal) userObject);
        }

        public boolean matchesItem(TreeItem item) {
            /* Only display this for leaf items */
            return item.getChildCount() == 0;
        }

        protected Widget configureWidget(final Goal goal) {
            /* get preference for this goal */
            final Preference pref;
            pref = (Preference) prefsByGoal.get(Integer.toString(goal.getId()));

            /* A panel for adding a new member */
            VerticalPanel vp = new VerticalPanel();

            final ListBox lb = new ListBox();
            fillAvailablePreferenceValues(lb, pref);   /* Also sets the selected index */
            vp.add(lb);

            Button btnAdd = new Button("Apply");
            btnAdd.addClickListener(new ClickListener() {
                public void onClick(Widget sender) {
                    int newValue = -1;
                    newValue = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
                    updatePreference(pref, newValue);
                }
            });
            vp.add(btnAdd);

            return vp;
        }

        protected void updatePreference(final Preference pref, int newValue) {
            if ( newValue == pref.getValue() )
                return;

            DataWriteService.App.getInstance().updatePreference(project.getId(), pref.getGoal().getId(), pref.getUser().getId(), newValue, container.getCurrentAuthToken(), this);
        }

        protected void fillAvailablePreferenceValues(final ListBox choices, Preference pref) {
            choices.addItem("0");
            choices.setSelectedIndex(0); /* This may get changed below */

            int currentVal = pref.getValue();
            int maxVal = currentVal + remainingBudget;
            for (int i = 1; i <= maxVal; i++ ) {
                choices.addItem(Integer.toString(i));
                if ( i == currentVal )
                    choices.setSelectedIndex(i);
            }
        }
    }
}